iT邦幫忙

2022 iThome 鐵人賽

DAY 14
0
Modern Web

致 JavaScript 開發者的 Functional Programming 新手指南系列 第 14

Day 14:什麼是純函式 ?(2):抽象化

  • 分享至 

  • xImage
  •  

在前一章節中,我們聊到如何透過撰寫純函式,來幫我們的函式進行優化。

然而在剛開始寫程式時,我其實很難將函式中一些重複性的概念抽離出來,於是會寫出一些「過於流程化」的程式碼,而這樣的習慣總是讓我寫出一些髒髒的程式碼,要碼不是在同一個函式中創造不需要的副作用,不然就是無法預期自己的程式碼會長得怎麼樣。

自從我開始使用純函式的概念優化程式碼後,這個壞習怪改了很多,程式碼不僅變得更加嚴謹,自己也越來越能將程式進行抽象化。

但問題來了,什麼是「抽象化」呢?

抽象化

關於如何進行更好的軟體設計、邏輯拆分,在電腦科學中有個理論可以更好協助理解 FP 究竟是怎麼協助我們進行程式碼的管理,這個理論是:抽象化(Abstraction)。

根據維基百科:「在電腦科學中,抽象化(英語:Abstraction)是將資料與程式,以它的語意來呈現出它的外觀,但是隱藏起它的實作細節。抽象化是用來減少程式的複雜度,使得程式設計師可以專注在處理少數重要的部份。」

而 Eric Elliott 在《Composing Software》一書中,針對抽象化提供了非常詳盡的解釋:

  1. 抽象化是一種拆解、簡化的過程。
  2. 抽象化就像是讓我們駕駛無人機一樣,自動化且安全。

抽象化的手段方法大致上可以分成兩種,廣義化(Generalization)與特殊化(Specialization)。

廣義化指的是在重複的程式碼中,找到這些程式碼的共同點,並且隱藏這些明顯重複的實作內容;而特殊化指的是只針對程式碼中的異處進行處理。

舉例來說純函式處理的就是廣義化的部分,至於特殊化的應用,我們在後續的柯里化章節中再進行更深入的了解。

在軟體中,抽象化可以有很多種形式:

  • 演算法
  • 資料結構
  • 模組
  • 類別(Classes)
  • 框架

當然,我們的主題是 FP ,抽象化當然也能套用在函式上。

以往初學程式時,我們可能都有聽過以下範例:

在物件導向設計中,一台車可能會有輪子元件、引擎元件、加速的方法、煞車的方法,我們會去定義這些屬性的概念,就好像一個製作車子的模型,只是這些模型不是一體成型的,而是由不同的小模型組合而成,當我們需要時,就可以依照這些模型自定義出我們想要的車子模型。
上述的過程同時物件導向設計中常稱的介面(interface)概念,雖然介面的實作會比我上述的概念更複雜一點,但總結來說定義介面就是一種「抽象化」概念,我們把一些可套用在其他車輛的元素封裝成一個元件,之後如果要新增一台車物件,我們就可以讓這台車去繼承或是複製這個「抽象化的模型」,降低需要製造新車的成本。

只不過在 FP 中我們是以函式作為抽象化的單位,而不像物件導向會以元件作為單位,那在撰寫純函式時,我們可以怎麼進行抽象化呢?

純函式中的抽象化

相較於物件導向設計,純函式雖然不會有「自己的狀態」,也不會有那些很具體可見的屬性能做抽象化,例如:我們既不會實體化元件,所以也比較難透過元件的概念還具象化我們的函式,這也是自己在初學 FP 時最初遇到的障礙。

雖然 FP 看起來並不如傳統的 UI 元件直覺,但我們依然可以在封裝函式時思考,有哪些「主體」或是「細節」是不會影響函式本身目的,所以可以被抽離出來的?

舉例來說,如果我們要設計一個函式是用來「榨出蘋果汁」,所以最一開始我們會從採收蘋果開始,到將蘋果切片,丟進榨汁機,然後要打幾秒,最後才能產生出一杯 600 c.c. 的蘋果汁。

讓我們來練習上述那段敘述的邏輯拆分:

函式細節 是否是函式本身要處理的問題? 原因
自己種蘋果?
超市買蘋果? x 不論是哪裡來的蘋果,都應該要可以榨出汁
蘋果切片?
蘋果磨塊? x 不論蘋果是否是切塊、切片、不切,都要能被榨出汁
丟進榨汁機 v 我們的目的是「榨出蘋果汁」,總不可能丟進電鍋裡吧?
要打幾秒 x 取決於使用者要喝什麼口感的果汁
一杯 600 c.c. x 取決使用多少蘋果來榨汁

結果我們拆解函式邏輯拆一拆發現,其實我們要的其實是一個果汁機!甚至要將什麼水果榨成汁都不是最重要的事。

雖然這個範例看起來有點荒謬,但自己覺得如 Eric Elliott 所述:「抽象化就是一種拆解的過程」,當你越觀察自己程式碼的細節,就越知道哪些東西應該要抽象化掉,去掉細節、留下真正該解決的問題。

於是最後我們獲得了一個超級精簡的純函式:

const blender = (fruit, amount) => `${amount} c.c. ${fruit} juice`;

如果這個果汁機將水果打小於一分鐘,會獲得奶昔般的口感,我們讓使用者決定要喝果汁還是果昔:

const blender = (fruit, amount, smoothie = false) => {
	if(smoothie) {
		return `${amount} c.c. ${fruit} smoothie`;
	}
	return `${amount} c.c. ${fruit} juice`;
};

在這邊我們可以將「是否要喝果汁還是果昔」想成「果汁打幾秒後會出現的結果」,最後讓我來驗證這個抽象化完的 blender 函式是否有符合 Pure Funciton 的準則?

const appleSmoothie1 = blender('apple', 600, true); // '600 c.c. apple smoothie'
const appleSmoothie2 = blender('apple', 600, true); // '600 c.c. apple smoothie'
const appleSmoothie3 = blender('apple', 400, true); // '400 c.c. apple smoothie'
const appleJuice = blender('apple', 600); // '600 c.c. apple juice'

經過驗證我們可以知道:

  1. blender 函式符合單輸入單輸出原則
  2. 沒有產生額外的副作用

如此我們就透過了「抽象化」的手法,將原本可能很複雜的函式,優化成了純函式。

我認為抽象化是 FP 中最難的一環,但好在我們可以在撰寫完程式碼後,透過拆解及觀察是否有無重複的細節,或是過多非必要的細節,來替我們的函式進行減量及抽象化,當程式碼複雜到一定程度時,甚至還可以拆解成兩個、多個不同的純函式。

當然抽象化的概念不止能應用在優化純函式中,更會在後續的內容中重複出現。

看到這邊,我們把純函式的核心概念給看了個大概,接著我們就針對文章內一直不斷重複提到的「副作用」來進行介紹吧!那我們下個章節見。

參考資料:

  1. Lessons in Abstraction: What FP Can Teach OOP
  2. Wikipedia:抽象化
  3. Eric Elliot (2019). Composing Software: An Exploration of Functional Programming and Object Composition in JavaScript(pp. 71).

上一篇
Day 13:什麼是純函式 ?(1):單輸入單輸出
下一篇
Day 15:什麼是純函式 ?(3):副作用
系列文
致 JavaScript 開發者的 Functional Programming 新手指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言